CLI Parameter Types
数据类型校验与转换
为 CLI parameter 添加类型提示后,Typer 会自动将参数转换成对应的数据类型
def main(name: str, age: int = 20, height_meters: float = 1.89, female: bool = True):
print(f"NAME is {name}, of type: {type(name)}")
print(f"--age is {age}, of type: {type(age)}")
print(f"--height-meters is {height_meters}, of type: {type(height_meters)}")
print(f"--female is {female}, of type: {type(female)}")
-
NAME
会被当做str
来看待 -
--age
会被转换成int
,--height-meters
会被转换成float
-
female
是一个bool
CLI option,所以--female
是 True,--no-female
则为 False

Number
使用 max
and min
来为 int 或 float 类型添加范围限制
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
age: Annotated[int, typer.Option(min=18)] = 20,
score: Annotated[float, typer.Option(max=100)] = 0,
):
print(f"ID is {id}")
print(f"--age is {age}")
print(f"--score is {score}")

使用上下限而不是报错
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0,
score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0,
):
print(f"ID is {id}")
print(f"--rank is {rank}")
print(f"--score is {score}")
count 类型
You can make a CLI option work as a counter with the count
parameter:
def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0):
print(f"Verbose level is {verbose}")
--verbose 就变成了一个计数类型的参数了

Boolean 类型
bool
类型的 CLI Options,Typer 会默认使用形如 --something
和 --no-something
来表示
当然,我们也是可以自定义这一行为的
禁用否定形式
def main(force: Annotated[bool, typer.Option("--force")] = False):
自定义否定形式
在 typer.Option
中传入一个字符串,用 /
隔开,分别表示肯定形式和否定形式
def main(accept: Annotated[Optional[bool], typer.Option("--accept/--reject")] = None):

短名
let's say we want -f
for --force
and -F
for --no-force
:
def main(force: Annotated[bool, typer.Option("--force/--no-force", "-f/-F")] = False):

只要否定形式
那就别写肯定形式
注意 /
前有空格!
def main(in_prod: Annotated[bool, typer.Option(" /--demo", " /-d")] = True):

不过这样写的话,注释说明就很难看得懂了
UUID
你可以将 CLI Option 定义为 UUID
from uuid import UUID
import typer
def main(user_id: UUID):
print(f"USER_ID is {user_id}")
print(f"UUID version is: {user_id.version}")
if __name__ == "__main__":
typer.run(main)

DateTime
from datetime import datetime
import typer
def main(birth: datetime):
print(f"Interesting day to be born: {birth}")
print(f"Birth hour: {birth.hour}")
if __name__ == "__main__":
typer.run(main)
Typer will accept any string from the following formats:
%Y-%m-%d
%Y-%m-%dT%H:%M:%S
%Y-%m-%d %H:%M:%S

自定义日期字符串格式
def main(
launch_date: Annotated[
datetime,
typer.Argument(
formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"]
),
],
):
print(f"Launch will be at: {launch_date}")
Enum
from enum import Enum
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
def main(network: NeuralNetwork = NeuralNetwork.simple):
print(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
typer.run(main)
Notice that the function parameter
network
will be anEnum
, not astr
.To get the
str
value in your function's code usenetwork.value
.

大小写敏感
默认情况下,Enum
类型的 CLI Parameter 是大小写敏感的,可以用 case_sensitive
来控制
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
def main(
network: Annotated[
NeuralNetwork, typer.Option(case_sensitive=False)
] = NeuralNetwork.simple,
):
print(f"Training neural network of type: {network.value}")

Enum 列表
A CLI parameter can also take a list of Enum
values:
class Food(str, Enum):
food_1 = "Eggs"
food_2 = "Bacon"
food_3 = "Cheese"
def main(groceries: Annotated[List[Food], typer.Option()] = [Food.food_1, Food.food_3]):
print(f"Buying groceries: {', '.join([f.value for f in groceries])}")

Path
You can declare a CLI parameter to be a standard Python pathlib.Path
.
This is what you would do for directory paths, file paths, etc:
from pathlib import Path
from typing import Optional
import typer
from typing_extensions import Annotated
def main(config: Annotated[Optional[Path], typer.Option()] = None):
if config is None:
print("No config file")
raise typer.Abort()
if config.is_file():
text = config.read_text()
print(f"Config file contents: {text}")
elif config.is_dir():
print("Config is a directory, will use all its config files")
elif not config.exists():
print("The config doesn't exist")
if __name__ == "__main__":
typer.run(main)

当然会有自动补全啦
校验选项
exists
: if set to true, 文件或目录必须得存在. If this is false 而且文件确实不存在,那之后的校验都不会进行了file_okay
: 校验是否是文件dir_okay
: 校验是否是目录writable
: if true, 会检查是否可写readable
: if true, 会检查是否可读resolve_path
: if this is true, 文件会被转换为绝对路径(软连接也会哦)- 带有
~
的路径不会被展开
- 带有
from pathlib import Path
import typer
from typing_extensions import Annotated
def main(
config: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
),
],
):
text = config.read_text()
print(f"Config file contents: {text}")
if __name__ == "__main__":
typer.run(main)

File
允许 CLI Parameters 以文件形式读 / 写文件
详见:https://typer.tiangolo.com/tutorial/parameter-types/file/
Custom Types
两种方式,使 Typer 支持你自定义的类:
- 定义
parser
函数来解析数据 - 定义 Click's custom types
import typer
from typing_extensions import Annotated
class CustomClass:
def __init__(self, value: str):
self.value = value
def __str__(self):
return f"<CustomClass: value={self.value}>"
def parse_custom_class(value: str):
return CustomClass(value * 2)
def main(
custom_arg: Annotated[CustomClass, typer.Argument(parser=parse_custom_class)],
custom_opt: Annotated[CustomClass, typer.Option(parser=parse_custom_class)] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")
if __name__ == "__main__":
typer.run(main)
import click
import typer
from typing_extensions import Annotated
class CustomClass:
def __init__(self, value: str):
self.value = value
def __repr__(self):
return f"<CustomClass: value={self.value}>"
class CustomClassParser(click.ParamType):
name = "CustomClass"
def convert(self, value, param, ctx):
return CustomClass(value * 3)
def main(
custom_arg: Annotated[CustomClass, typer.Argument(click_type=CustomClassParser())],
custom_opt: Annotated[
CustomClass, typer.Option(click_type=CustomClassParser())
] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")
if __name__ == "__main__":
typer.run(main)